/*
 * Copyright (c) 2011-2021, baomidou (jobob@qq.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.boot.persistence.service;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.boot.persistence.condition.QueryCondition;
import com.boot.persistence.mapper.IMapper;
import com.boot.persistence.model.BaseModel;
import com.boot.persistence.model.ReadOnlyModel;
import com.boot.web.exception.GlobalException;
import com.boot.web.http.Status;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.session.SqlSession;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.boot.persistence.service.QueryConditionHelper.*;

/**
 * @author 霜寒 <1621856595@qq.com>
 * @description IService 实现类，修改自 {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl}
 * 尽量使用 mapper 操作，防止缓存脏读、脏写 或者使用 IService.super.xxx
 * @date 2021/6/20 14:17
 **/
@Slf4j
public abstract class ServiceImpl<Id extends Serializable, T extends BaseModel<Id>, Mapper extends IMapper<T>> implements IService<Id, T, Mapper> {

    protected Mapper mapper;
    protected Class<Id> idClass;
    protected Class<T> modelClass;
    protected Class<Mapper> mapperClass;

    public ServiceImpl(Mapper mapper) {
        this.mapper = mapper;
        this.idClass = currentIdClass();
        this.modelClass = currentModelClass();
        this.mapperClass = currentMapperClass();
    }

    @Override
    public Mapper getMapper() {
        return mapper;
    }

    @Override
    public Class<T> getModelClass() {
        return modelClass;
    }

    @Override
    public Class<Id> getIdClass() {
        return idClass;
    }

    @SuppressWarnings("unchecked")
    protected Class<Id> currentIdClass() {
        return (Class<Id>) ReflectionKit.getSuperClassGenericType(getClass(), ServiceImpl.class, 0);
    }

    @SuppressWarnings("unchecked")
    protected Class<T> currentModelClass() {
        return (Class<T>) ReflectionKit.getSuperClassGenericType(getClass(), ServiceImpl.class, 1);
    }

    @SuppressWarnings("unchecked")
    protected Class<Mapper> currentMapperClass() {
        return (Class<Mapper>) ReflectionKit.getSuperClassGenericType(getClass(), ServiceImpl.class, 2);
    }

    @Override
    public IPage<T> select(QueryCondition<Id, T> condition) {
        Page<T> page = getPage(condition.getPageCondition());
        QueryWrapper<T> wrapper = getOrders(condition.getOrderConditions(), null);
        wrapper = getColumns(condition.getColumnCondition(), wrapper);
        return page(page, wrapper.setEntity(condition.getQuery()));
    }

    @Override
    public IPage<T> selectLike(QueryCondition<Id, T> condition) {
        Page<T> page = getPage(condition.getPageCondition());
        QueryWrapper<T> wrapper = getOrders(condition.getOrderConditions(), null);
        wrapper = getColumns(condition.getColumnCondition(), wrapper);
        wrapper = like(condition.getQuery(), wrapper);
        return page(page, wrapper);
    }

    /**
     * 批量插入
     *
     * @param modelList ignore
     * @param batchSize ignore
     * @return ignore
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveBatch(Collection<T> modelList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
        return executeBatch(modelList, batchSize, (sqlSession, model) -> sqlSession.insert(sqlStatement, model));
    }

    /**
     * 获取mapperStatementId
     *
     * @param sqlMethod 方法名
     * @return 命名id
     * @since 3.4.0
     */
    protected String getSqlStatement(SqlMethod sqlMethod) {
        return SqlHelper.getSqlStatement(mapperClass, sqlMethod);
    }

    /**
     * TableId 注解存在更新记录，否插入一条记录
     *
     * @param model 实体对象
     * @return boolean
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveOrUpdate(T model) {
        if (null != model) {
            Id id = model.pk();
            return StringUtils.checkValNull(id) || Objects.isNull(mapper.selectById(id)) ?
                    IService.super.save(model) : IService.super.updateById(model);
        }
        return false;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveOrUpdateBatch(Collection<T> modelList, int batchSize) {
        return SqlHelper.saveOrUpdateBatch(this.modelClass, this.mapperClass, modelList, batchSize, (sqlSession, model) -> {
            Id id = model.pk();
            return StringUtils.checkValNull(id)
                    || CollectionUtils.isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), model));
        }, (sqlSession, model) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, model);
            sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param);
        });
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateBatchById(Collection<T> modelList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
        return executeBatch(modelList, batchSize, (sqlSession, model) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, model);
            sqlSession.update(sqlStatement, param);
        });
    }

    @Override
    public T getOne(Wrapper<T> queryWrapper, boolean throwEx) {
        if (throwEx) {
            return mapper.selectOne(queryWrapper);
        }
        return SqlHelper.getObject(mapper.selectList(queryWrapper));
    }

    @Override
    public Map<String, Object> getMap(Wrapper<T> queryWrapper) {
        return SqlHelper.getObject(mapper.selectMaps(queryWrapper));
    }

    @Override
    public <V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper) {
        return SqlHelper.getObject(listObjs(queryWrapper, mapper));
    }

    /**
     * 执行批量操作
     *
     * @param list      数据集合
     * @param batchSize 批量大小
     * @param consumer  执行方法
     * @param <E>       泛型
     * @return 操作结果
     * @since 3.3.1
     */
    protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
        return SqlHelper.executeBatch(this.modelClass, list, batchSize, consumer);
    }

    /**
     * 执行批量操作（默认批次提交数量{@link IService#DEFAULT_BATCH_SIZE}）
     *
     * @param list     数据集合
     * @param consumer 执行方法
     * @param <E>      泛型
     * @return 操作结果
     * @since 3.3.1
     */
    protected <E> boolean executeBatch(Collection<E> list, BiConsumer<SqlSession, E> consumer) {
        return executeBatch(list, DEFAULT_BATCH_SIZE, consumer);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateById(T model) {
        if (ReadOnlyModel.class.isAssignableFrom(this.modelClass)) {
            ReadOnlyModel target = (ReadOnlyModel) mapper.selectById(model.pk());
            if (null == target) {
                throw new GlobalException(Status.NO_RECORD, HttpStatus.NOT_FOUND);
            }
            if (target.readOnly()) {
                throw new GlobalException(Status.READONLY_FAILURE, HttpStatus.BAD_REQUEST);
            }
        }
        return IService.super.updateById(model);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateBatchById(Collection<T> modelList) {
        List<Serializable> idList = modelList.stream().map(T::pk).collect(Collectors.toList());
        List<T> list = mapper.selectBatchIds(idList);
        if (list.size() == modelList.size()) {
            return IService.super.updateBatchById(modelList);
        } else {
            throw new GlobalException(Status.NO_RECORD, HttpStatus.NOT_FOUND);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean removeById(Id id) {
        if (ReadOnlyModel.class.isAssignableFrom(this.modelClass)) {
            ReadOnlyModel target = (ReadOnlyModel) mapper.selectById(id);
            if (null == target) {
                return false;
            }
            if (target.readOnly()) {
                throw new GlobalException(Status.READONLY_FAILURE, HttpStatus.BAD_REQUEST);
            }
        }
        return mapper.deleteById(id) > 0;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean removeByIds(Collection<? extends Id> idList) {
        if (ReadOnlyModel.class.isAssignableFrom(this.modelClass)) {
            idList.forEach(id -> {
                ReadOnlyModel target = (ReadOnlyModel) mapper.selectById(id);
                if (target.readOnly()) {
                    throw new GlobalException(Status.READONLY_FAILURE, HttpStatus.BAD_REQUEST);
                }
            });
        }
        return IService.super.removeByIds(idList);
    }

    abstract public T createModel();

}
